﻿/* AgileFx Version 1.0
 * An open-source framework for rapid development of applications and services using Microsoft.net
 * Developed by: AgileHead
 * Website: www.agilefx.org
 * This component is licensed under the terms of the Apache 2.0 License.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using AgileFx.EF.ModificationTypes;

namespace AgileFx.EF
{
    public class DbContext
    {
        IObjectContext objContext;

        //This will turn off context insertion of you don't need it.
        //This will probably vanish, at things get more settled.
        static bool disableContextInsertion = false;
        public static bool DisableContextInsertion
        {
            get { return disableContextInsertion; }
            set { disableContextInsertion = value; }
        }

        public bool ContextInsertionDisabledOnInstance { get; set; }

        public DbContext()
        {
            this.objContext = new EFObjectContextWrapper(Config.GetObjectContext());
        }

        public DbContext(IObjectContext objContext)
        {
            this.objContext = objContext;
        }

        /*
         * Returns the object query, which decides the inner objects which should be preloaded.
         * This is obtained from the attribute defined on the entity.
         */

        public ObjectQuery<T> GetObjectQuery<T>() where T : EntityObject, new()
        {
            return GetObjectQuery<T>(LoadRelatedByDefault<T>());
        }

        public ObjectQuery<T> GetObjectQuery<T>(params Expression<Func<T, object>>[] includedFields) where T : EntityObject, new()
        {
            var query = GetObjectQuery<T>(LoadRelatedByDefault<T>());
            foreach (var include in includedFields)
                query = query.Include(ExpressionUtil.GetPropertyName(include));

            return query;
        }

        public ObjectQuery<T> GetObjectQuery<T>(bool loadRelated) where T : EntityObject, new()
        {
            ObjectQuery<T> query = null;
            if (typeof(T).BaseType != typeof(EntityObject))
            {
                var baseQuery = objContext.GetType().GetMethod("CreateQuery").MakeGenericMethod(typeof(T).BaseType).Invoke(objContext, new object[] { string.Format("[{0}]", typeof(T).BaseType.Name), new ObjectParameter[0] });
                query = baseQuery.GetType().GetMethod("OfType").MakeGenericMethod(typeof(T)).Invoke(baseQuery, null) as ObjectQuery<T>;
            }
            else
            {
                query = objContext.CreateQuery<T>(string.Format("[{0}]", typeof(T).Name));
            }

            if (loadRelated)
            {
                foreach (var inc in GetIncludes<T>())
                    query = query.Include(inc);
            }

            return query;
        }

        public ObjectQuery<T> GetObjectQuery<T>(bool loadRelated, params Expression<Func<T, object>>[] includedFields)
            where T : EntityObject, new()
        {
            var query = objContext.CreateQuery<T>(string.Format("[{0}]", typeof(T).Name));

            if (loadRelated)
            {
                foreach (var inc in GetIncludes<T>())
                    query = query.Include(inc);
            }

            foreach (var include in includedFields)
                query = query.Include(ExpressionUtil.GetPropertyName(include));

            return query;
        }

        /*
         * Decides which fields of the entity needs to be loaded.
         */
        private static EntityLoadSettings<T> GetEntityLoadSettings<T>()
        {
            object[] atts = typeof(T).GetCustomAttributes(typeof(EntityLoadSettingsAttribute), false);
            if (null != atts && atts.Length > 0 && null != atts[0])
            {
                EntityLoadSettingsAttribute att = (EntityLoadSettingsAttribute)atts[0];
                return Activator.CreateInstance(att.Value) as EntityLoadSettings<T>;
            }
            return null;
        }

        /*
         * Get methods take a predicate and return an entity
         */
        public T Get<T>(Expression<Func<T, bool>> filter) where T : EntityObject, new()
        {
            return Get<T>(filter, LoadRelatedByDefault<T>());
        }

        public T Get<T>(Expression<Func<T, bool>> filter, params Expression<Func<T, object>>[] includedFields)
            where T : EntityObject, new()
        {
            var query = GetObjectQuery<T>(includedFields);
            T t = query.Where(filter).First();
            AttachContext(t);
            return t;
        }

        public T Get<T>(Expression<Func<T, bool>> filter, bool loadRelated) where T : EntityObject, new()
        {
            var objQuery = GetObjectQuery<T>(loadRelated);
            T t = objQuery.Where(filter).First();
            AttachContext(t);
            return t;
        }

        public T Get<T>(Expression<Func<T, bool>> filter, bool loadRelated, params Expression<Func<T, object>>[] includedFields)
            where T : EntityObject, new()
        {
            var query = GetObjectQuery<T>(loadRelated, includedFields);
            T t = query.Where(filter).First();
            AttachContext(t);
            return t;
        }

        /*
         * If the value does not exist, returns null.
         */
        public T GetOrDefault<T>(Expression<Func<T, bool>> filter) where T : EntityObject, new()
        {
            return GetOrDefault<T>(filter, LoadRelatedByDefault<T>()) ?? null;
        }

        public T GetOrDefault<T>(Expression<Func<T, bool>> filter, params Expression<Func<T, object>>[] includedFields)
            where T : EntityObject, new()
        {
            var query = GetObjectQuery<T>(includedFields);
            T t = query.Where(filter).FirstOrDefault();
            AttachContext(t);
            return t;
        }

        public T GetOrDefault<T>(Expression<Func<T, bool>> filter, bool loadRelated) where T : EntityObject, new()
        {
            var query = GetObjectQuery<T>(loadRelated);
            T t = query.Where(filter).FirstOrDefault();
            AttachContext(t);
            return t;
        }

        public T GetOrDefault<T>(Expression<Func<T, bool>> filter, bool loadRelated, params Expression<Func<T, object>>[] includedFields)
            where T : EntityObject, new()
        {
            var query = GetObjectQuery<T>(loadRelated, includedFields);
            T t = query.Where(filter).FirstOrDefault();
            AttachContext(t);
            return t;
        }

        /*
         * Takes a predicate and returns a matching list
         */
        public List<T> GetList<T>() where T : EntityObject, new()
        {
            return GetList<T>(LoadRelatedByDefault<T>());
        }

        public List<T> GetList<T>(bool loadRelated) where T : EntityObject, new()
        {
            return GetList<T>(null, loadRelated);
        }

        public List<T> GetList<T>(Expression<Func<T, bool>> filter) where T : EntityObject, new()
        {
            return GetList<T>(filter, LoadRelatedByDefault<T>());
        }

        public List<T> GetList<T>(Expression<Func<T, bool>> filter, params Expression<Func<T, object>>[] includedFields)
            where T : EntityObject, new()
        {
            var objQuery = GetObjectQuery<T>(includedFields);
            IQueryable<T> query = (null == filter) ? objQuery : objQuery.Where(filter);
            List<T> results = query.ToList();
            AttachContext(results);
            return results;
        }

        public List<T> GetList<T>(Expression<Func<T, bool>> filter, bool loadRelated) where T : EntityObject, new()
        {
            var objQuery = GetObjectQuery<T>(loadRelated);
            IQueryable<T> query = (null == filter) ? objQuery : objQuery.Where(filter);
            List<T> results = query.ToList();
            AttachContext(results);
            return results;
        }

        public List<T> GetList<T>(Expression<Func<T, bool>> filter, bool loadRelated, params Expression<Func<T, object>>[] includedFields)
            where T : EntityObject, new()
        {
            var objQuery = GetObjectQuery<T>(loadRelated, includedFields);
            IQueryable<T> query = (null == filter) ? objQuery : objQuery.Where(filter);
            List<T> results = query.ToList();
            AttachContext(results);
            return results;
        }

        /*
         * Returns a partial list, useful for paging
         */

        public PartialList<T> GetPartialList<T>(int startNum, int maxResults, Expression<Func<T, bool>> filter,
            Func<IQueryable<T>, IOrderedQueryable<T>> sort) where T : EntityObject, new()
        {
            return GetPartialList<T>(startNum, maxResults, filter, sort, LoadRelatedByDefault<T>());
        }

        public PartialList<T> GetPartialList<T>(int startNum, int maxResults, Expression<Func<T, bool>> filter,
            Func<IQueryable<T>, IOrderedQueryable<T>> sort, bool loadRelated) where T : EntityObject, new()
        {
            var objQuery = GetObjectQuery<T>(loadRelated);

            IQueryable<T> query = (null == filter) ? objQuery : objQuery.Where(filter);
            PartialList<T> results = sort(query).ToPartialList<T>(startNum, maxResults);
            AttachContext(results);
            return results;
        }

        public PartialList<T> GetPartialList<T>(int startNum, int maxResults, Expression<Func<T, bool>> filter,
            Func<IQueryable<T>, IOrderedQueryable<T>> sort, params Expression<Func<T, object>>[] includedFields) where T : EntityObject, new()
        {
            return GetPartialList<T>(startNum, maxResults, filter, sort, LoadRelatedByDefault<T>(), includedFields);
        }

        public PartialList<T> GetPartialList<T>(int startNum, int maxResults, Expression<Func<T, bool>> filter,
            Func<IQueryable<T>, IOrderedQueryable<T>> sort, bool loadRelated, params Expression<Func<T, object>>[] includedFields) where T : EntityObject, new()
        {
            var objQuery = GetObjectQuery<T>(loadRelated, includedFields);

            IQueryable<T> query = (null == filter) ? objQuery : objQuery.Where(filter);
            PartialList<T> results = sort(query).ToPartialList<T>(startNum, maxResults);
            AttachContext(results);
            return results;
        }

        //Attaches Context for ContextAware Objects
        private void _attachContextToObject(Object o)
        {
            _attachContextToObject(o, new List<object>());
        }

        //Attaches Context for ContextAware Objects
        private void _attachContextToObject(Object o, List<object> alreadyProcessed)
        {
            alreadyProcessed.Add(o);
            if (o != null && o is EntityObject)
            {
                var contextAwareEntity = o as IContextAware;
                if (contextAwareEntity != null)
                {
                    //if there is an existing context, leave it alone.
                    //.. but if so, it means that we may be in trouble. :)
                    if (contextAwareEntity.Context == null)
                        contextAwareEntity.Context = this;
                }

                //Traverse properties and Attach to all of them
                foreach (var p in o.GetType().GetProperties())
                {
                    object fieldValue = p.GetValue(o, null);

                    if (fieldValue != null)
                    {
                        if (fieldValue is EntityObject && !alreadyProcessed.Contains(fieldValue))
                            _attachContextToObject(fieldValue, alreadyProcessed);

                        //If it is a collection, loop through the collection.
                        if (fieldValue is IEnumerable && fieldValue is RelatedEnd)
                        {
                            var fieldCollection = (fieldValue as IEnumerable);
                            foreach (object collectionItem in fieldCollection)
                            {
                                if (collectionItem is EntityObject && !alreadyProcessed.Contains(collectionItem))
                                    _attachContextToObject(collectionItem, alreadyProcessed);
                            }
                        }
                    }
                }
            }
        }

        private void AttachContext<T>(T t) where T : EntityObject, new()
        {
            if (!disableContextInsertion && !ContextInsertionDisabledOnInstance)
                _attachContextToObject(t);
        }

        private void AttachContext<T>(List<T> listOfT) where T : EntityObject, new()
        {
            foreach (T t in listOfT)
            {
                AttachContext(t);
            }
        }

        private void AttachContext<T>(PartialList<T> listOfT) where T : EntityObject, new()
        {
            foreach (T t in listOfT)
            {
                AttachContext(t);
            }
        }

        //Execute in context
        public static void Execute(Action<DbContext> f)
        {
            DbContext ctx = new DbContext();
            f(ctx);
            ctx.SaveChanges();
        }

        /*
         * Delete an object
         */
        internal void DeleteObject(EntityObject obj)
        {
            objContext.DeleteObject(obj);
        }

        /*
         * Saves changes to the database
         */
        public void SaveChanges()
        {
            objContext.SaveChanges();
        }

        /*
         * Attaches context to Context-Aware entities
         */
        public void AttachToEntity<T>(T t) where T : EntityObject, new()
        {
            if (t is IContextAware && t != null)
                (t as IContextAware).Context = this;
        }

        public static void DetachFromEntity(IContextAware obj)
        {
            obj.Context = null;
        }

        /*
         * Decides whether Related objects are loaded by default.
         */

        private static bool LoadRelatedByDefault<T>()
        {
            var settings = GetEntityLoadSettings<T>();
            return (null == settings) ? false : settings.LoadRelatedByDefault;
        }

        private static string[] GetIncludes<T>()
        {
            var incExps = GetEntityLoadSettings<T>().Includes;
            return (null == incExps) ? default(string[]) : incExps.Select(x => ExpressionUtil.GetPropertyName(x)).ToArray();
        }

        //Get the EntitySetName
        private static string GetEntitySetName(object obj)
        {
            return obj.GetType().Name;
        }

        public T Create<T>(IModification<T> mod) where T : EntityObject, new()
        {
            var t = mod.ApplyChanges(null, this);
            objContext.AddObject(GetEntitySetName(t), t);
            return t;
        }

        public void AddObject<T>(T t) where T : EntityObject, new()
        {
            objContext.AddObject(GetEntitySetName(t), t);
        }

        public TElement ApplyChanges<TElement>(object obj,
            IModification<TElement> mod)
            where TElement : EntityObject, new()
        {
            var element = mod.ApplyChanges(obj, this);
            if (mod.Type == ModificationType.Add && null == element.EntityKey)
                objContext.AddObject(GetEntitySetName(element), element);
            return element;
        }
    }
}